/**
 * @author David Young
 */
package csab;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.newdawn.slick.Color;
import org.newdawn.slick.Font;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.tiled.*;
import org.newdawn.slick.util.Log;

import csab.core.Collidable;
import csab.core.Entity;
import csab.core.PlayerStats;
import csab.core.ResourceManager;
import csab.core.Sprite;
import csab.core.TerrainCollidable;
import csab.core.View;
import csab.core.structs.QuadTree;
import csab.core.utility.SpawnerFactory;
import csab.game.sprites.PlayerSprite;

/**
 * This class is designed to simulate the game world
 * and represents a level within the game.
 * @author David Young
 */
public class LevelBase {
	public static final String COLLISION_GROUPN = "collision";
	public static final String SPAWN_GROUPN = "spawn";
	public static final String SPRITE_GROUPN = "sprite";
	
	public static final Integer DEFAULT_GRAVITY = 5;
	public static final Integer DEFAULT_HASFOREGROUND = 0;
	public static final Integer DEFAULT_ISPLATFORM = 0;
	
	private List<Collidable> collidablelist;
	private List<SpawnerFactory> spawnfactories;
	private float gravity;
	private boolean hasForeground;
	private QuadTree le_tree;
	private TiledMapPlus levelmap;
	
	private Font HUDFONT;
	private static final String HUD_FONT_TYPE = "speech";
	
	private String mapname;
	
	private PlayerSprite player;
	private List<Sprite> spritelist;
	private View viewport;
	private boolean playerDied;
	
	
	/**
	 * Constructor for this class.
	 * @param filename The .tmx (Tiled map file) to load.
	 * @throws SlickException 
	 * @see org.newdawn.slick.tiled.TiledMap
	 * @see org.newdawn.slick.tiled.TiledMapPlus
	 */
	public LevelBase(String filename) throws SlickException
	{
		playerDied = false;
		levelmap = new TiledMapPlus(filename);
		mapname = levelmap.getMapProperty("name", "NO NAME");
		ResourceManager mgr = ResourceManager.getManager("resource/resources.xml");
		HUDFONT = mgr.getFont(HUD_FONT_TYPE);
		
		try {
			gravity = Float.parseFloat(levelmap.getMapProperty("gravity", DEFAULT_GRAVITY.toString()));
		}
		catch(NumberFormatException e)
		{
			Log.warn("\t-Missing property gravity");
			gravity = DEFAULT_GRAVITY;
		}
		
		try {
			hasForeground = (Integer.parseInt(
					levelmap.getMapProperty("hasForeground", DEFAULT_HASFOREGROUND.toString())) == 1);
		}
		catch(NumberFormatException e)
		{
			Log.warn("\t-Missing property hasForeground");
			hasForeground = false;
		}
		
		Log.debug("Loaded map :: " + mapname);
		Log.debug("\t|_ gravity [" + gravity + "]");
		Log.debug("\t|_ hasForeground [" + hasForeground + "]");
		Log.debug("\t|_ Map Size (X: " + levelmap.getWidth() * levelmap.getTileWidth() +
				  ", Y: " + levelmap.getHeight() * levelmap.getTileHeight() + ")");
		
		Entity.setWorldXRange(0, levelmap.getWidth() * levelmap.getTileWidth());
		Entity.setWorldYRange(0, levelmap.getHeight() * levelmap.getTileHeight());
	}
	
	/**
	 * Determine whether or not the player is dead.
	 * @return true if the player is dead.
	 */
	public boolean isPlayerDead()
	{
		return playerDied;
	}
	
	/**
	 * Get the level map managed by this class.
	 * @return The TiledMap level map of this class.
	 */
	public final TiledMapPlus getLevelMap() { 
		return levelmap; 
	}
	
	/**
	 * Initialize this level based on the provided
	 * GameContainer context.
	 * @param gc The GameContainer context to initialize
	 * this class from.
	 * @see org.newdawn.slick.GameContainer
	 */
	public void init(GameContainer gc) throws SlickException
	{
		int mapw = levelmap.getWidth() * levelmap.getTileWidth();
		int maph = levelmap.getHeight() * levelmap.getTileHeight();
		viewport = new View(0, 0, gc.getWidth(), gc.getHeight(), mapw, maph);
		
		spritelist = getSpriteObjects();
		spawnfactories = getSpawnObjects();
		collidablelist = getCollisionObjects();
		
		le_tree = new QuadTree(mapw, maph);
		
		player = getPlayerSpawn();
		
		//Add entities to QuadTree
		le_tree.addAll(collidablelist);
		le_tree.add(player);
		Iterator<Sprite> itr = spritelist.iterator();
		while(itr.hasNext())
		{
			le_tree.add(itr.next());
		}
		
		//Perform initial collision tests
		le_tree.doCollisions(player);
		itr = spritelist.iterator();
		while(itr.hasNext())
		{
			le_tree.doCollisions(itr.next());
		}
	}
	
	/**
	 * Cleanup any resource allocated for this level.
	 * @throws SlickException 
	 */
	public void cleanup() throws SlickException {
		for(Sprite sprite : spritelist)
		{
			sprite.doDestroy();
		}
		for(Collidable c : collidablelist)
		{
			c.doDestroy();
		}
		
		player.doDestroy();
		
		ResourceManager mgr = ResourceManager.getManager("resource/resources.xml");
		mgr.freeFont(HUD_FONT_TYPE);
	}
	
	/**
	 * Render the level.
	 * @param gc The GameContainer of the current game context.
	 * @param g The current Graphics context.
	 * @see org.newdawn.slick.Graphics
	 * @see org.newdawn.slick.GameContainer
	 */
	public void render(GameContainer gc, Graphics g)
	{
		viewport.center(player);
		if(hasForeground)
		{
			for(int i = 0; i < levelmap.getLayerCount() - 1; i++)
			{
				viewport.renderTileMap(levelmap, i);
			}

			viewport.drawEntities(g, le_tree);
			viewport.renderTileMap(levelmap, levelmap.getLayerCount() - 1);
		}
		else
		{
			viewport.renderTileMap(levelmap);
			viewport.drawEntities(g, le_tree);
		}
		String health = "Health :: " + player.getHealth() + "/" + player.getMaxHealth();
		HUDFONT.drawString(10, 10 + HUDFONT.getHeight(health), health, Color.yellow);
	}
	
	/**
	 * Update the level (entities, etc.)
	 * @param gc The GameContainer of the current game context.
	 * @param delta The ticks since the last game update.
	 * @see org.newdawn.slick.GameContainer
	 */
	public void update(GameContainer gc, int delta) throws SlickException
	{	
		if(player.needsCleanup())
			playerDied = true;
		//Update the spawn list
		Iterator<SpawnerFactory> sitr = spawnfactories.iterator();
		while(sitr.hasNext())
		{
			List<Sprite> append = new LinkedList<Sprite>();
			sitr.next().update(append, delta);
			
			if(append.size() > 0)
			{
				spritelist.addAll(append);
				for(Sprite temp : append)
				{
					le_tree.add(temp);
				}
			}
		}
		
		//Perform movement and/or perform an action
		player.doAction(delta);
		player.getController().doBehavior(delta, null, gc.getInput());
		player.doMove(delta, 0, gravity);
		
		Iterator<Sprite> itr = spritelist.iterator();
		while(itr.hasNext())
		{
			//Does the sprite need to be removed from the game?
			Sprite spr = itr.next();
			if(spr.needsCleanup())
			{
				itr.remove();
				le_tree.removeItem(spr);
				spr.doDestroy();
				continue;
			}
			
			//Perform a per-update action
			spr.doAction(delta);
			//Handle AI/player based input
			spr.getController().doBehavior(delta, player, gc.getInput());
			spr.doMove(delta, 0, gravity);
		}
		
		//Perform collisions
		le_tree.doCollisions(player);
		itr = spritelist.iterator();
		while(itr.hasNext())
		{
			Sprite temp = itr.next();
			le_tree.doCollisions(temp);
		}
	}
	
	/*
	 * @returns List of "collision" objects for this map/level.
	 */
	private final List<Collidable> getCollisionObjects() throws SlickException
	{
		ObjectGroup grp = null;
		try {
			grp = levelmap.getObjectGroup(COLLISION_GROUPN);
		}
		catch(NullPointerException e)
		{
			String msg = "\t|_ No sprites specified!";
			Log.debug(msg);
			return new LinkedList<Collidable>();
		}
		
		List<GroupObject> objs = null;
		if(grp != null)
			objs = grp.getObjects();
		
		if(objs == null || objs.size() == 0)
		{
			String msg = "\t|_ No collision boxes specified!";
			Log.debug(msg);
			return new LinkedList<Collidable>();
		}
		
		List<Collidable> rtn = new ArrayList<Collidable>(objs.size());
		Log.debug("\t|_ Number of terrain collision boxes [" + objs.size() + "]");
		
		for(GroupObject item : objs)
		{
			boolean isPlatform = false;
			int width = 0;
			int height = 0;
			try {
				isPlatform = (Integer.parseInt(
						item.props.getProperty("isPlatform", DEFAULT_ISPLATFORM.toString())) == 1);
			}
			catch(NumberFormatException e)
			{
				Log.warn("\t\t-Missing property isPlatform");
				isPlatform = false;
			}
			
			try {
				width = (Integer.parseInt(
						item.props.getProperty("width"))) * levelmap.getTileWidth();
			}
			catch(NumberFormatException e)
			{
				Log.error("\t\t|_ Missing property width! Object not parsed.");
				continue;
			}
			
			try {
				height = (Integer.parseInt(
						item.props.getProperty("height"))) * levelmap.getTileHeight();
			}
			catch(NumberFormatException e)
			{
				Log.error("\t\t|_ Missing property height! Object not parsed.");
				continue;
			}
			
			int x = item.x, y = item.y;
			Log.debug("\t\t|_ TerrainCollidable [" + x + ", " + y + ", " + width + ", " + height + ", " + isPlatform +"]");
			rtn.add(new TerrainCollidable(x, y, width, height, isPlatform));
		}
		return rtn;
	}
	
	/*
	 * @returns List of "spawn" objects for this map/level.
	 */
	private final List<SpawnerFactory> getSpawnObjects() throws SlickException
	{
		ObjectGroup grp = null;
		try {
			grp = levelmap.getObjectGroup(SPAWN_GROUPN);
		}
		catch(NullPointerException e)
		{
			String msg = "\t|_ No sprites specified!";
			Log.debug(msg);
			return new LinkedList<SpawnerFactory>();
		}
		
		List<GroupObject> objs = null;
		if(grp != null)
			objs = grp.getObjects();
		
		if(objs == null || objs.size() == 0)
		{
			String msg = "\t|_ No spawners specified!";
			Log.debug(msg);
			return new LinkedList<SpawnerFactory>();
		}
		
		List<SpawnerFactory> rtn = new ArrayList<SpawnerFactory>(objs.size() + 1);
		Log.debug("\t|_ Number of spawners [" + objs.size() + "]");
		
		for(GroupObject item : objs)
		{
			int x = item.x;
			int y = item.y;
			String type = item.props.getProperty("spawntype", "NULL");
			
			if(type.compareToIgnoreCase("NULL") == 0)
			{
				Log.error("\t\t|_ Missing property type! Object not parsed.");
				continue;
			}
			
			int minspawns = -1;
			try {
				minspawns = (Integer.parseInt(
						item.props.getProperty("minspawns", "0")));
			}
			catch(NumberFormatException e)
			{
				Log.error("\t\t|_ Invalid property minspawns! Object not parsed.");
				continue;
			}
			
			int maxspawns = -1;
			try {
				maxspawns = (Integer.parseInt(
						item.props.getProperty("maxspawns", "0")));
			}
			catch(NumberFormatException e)
			{
				Log.error("\t\t|_ Invalid property maxspawns! Object not parsed.");
				continue;
			}
			
			int totalspawns = -1;
			try {
				totalspawns = (Integer.parseInt(
						item.props.getProperty("totalspawns", "0")));
			}
			catch(NumberFormatException e)
			{
				Log.error("\t\t|_ Invalid property totalspawns! Object not parsed.");
				continue;
			}
			
			int mintimebetween = -1;
			try {
				mintimebetween = (Integer.parseInt(
						item.props.getProperty("mintime", "500")));
			}
			catch(NumberFormatException e)
			{
				Log.error("\t\t|_ Invalid property mintime! Object not parsed.");
				continue;
			}
			
			float chance = -1.0f;
			try {
				chance = (Float.parseFloat(
						item.props.getProperty("chance", "1.0")));
				chance = Math.min(chance, 1.0f);
			}
			catch(NumberFormatException e)
			{
				Log.error("\t\t|_ Invalid property mintime! Object not parsed. " +
						  e.getMessage());
				continue;
			}
			
			
			SpawnerFactory factory = new SpawnerFactory(minspawns, maxspawns, totalspawns, 
														mintimebetween, chance, type);
			factory.setX(x);
			factory.setY(y);
			rtn.add(factory);
		}
		return rtn;
	}
	
	private final PlayerSprite getPlayerSpawn() throws SlickException
	{
		ObjectGroup grp = null;
		try {
			grp = levelmap.getObjectGroup("player");
		}
		catch(NullPointerException e)
		{
			String msg = "\t|_ No player spawn specified!";
			Log.error(msg);
			throw new SlickException("Define a player spawn!");
		}
		
		List<GroupObject> objs = null;
		if(grp != null)
			objs = grp.getObjects();
		
		if(objs.size() != 1)
		{
			String msg = "\t|_ No player spawn specified!";
			Log.error(msg);
			throw new SlickException("Define only one player spawn!");
		}
		
		GroupObject ploc = objs.get(0);
		PlayerSprite play = new PlayerSprite("Player");
		play.setPos(ploc.x, ploc.y);
		play.setName(PlayerStats.getPlayerName());
		play.setMaxHealth(PlayerStats.getHealth());
		play.giveHealth(PlayerStats.getHealth());
		play.setCanCollide(true);
		play.setDrawable(true);
		return play;
	}
	
	private final List<Sprite> getSpriteObjects() throws SlickException
	{
		ObjectGroup grp = null;
		try {
			grp = levelmap.getObjectGroup(SPRITE_GROUPN);
		}
		catch(NullPointerException e)
		{
			String msg = "\t|_ No sprites specified!";
			Log.debug(msg);
			return new LinkedList<Sprite>();
		}
		
		List<GroupObject> objs = null;
		if(grp != null)
			objs = grp.getObjects();
		
		if(objs == null || objs.size() == 0)
		{
			String msg = "\t|_ No sprites specified!";
			Log.debug(msg);
			return new LinkedList<Sprite>();
		}
		
		ResourceManager mgr = ResourceManager.getManager("resource/resources.xml");

		List<Sprite> rtn = new ArrayList<Sprite>(objs.size() + 1);
		Log.debug("\t|_ Number of sprites [" + objs.size() + "]");
		
		for(GroupObject item : objs)
		{
			String type = item.props.getProperty("type", "");
			int x = item.x;
			int y = item.y;
			if(type.compareToIgnoreCase("") == 0)
			{
				Log.error("\t\t|_ Could not parse sprite - no type!");
				continue;
			}
			
			Sprite spr = null;
			try {
				spr = mgr.getSprite(type);
			}
			catch(SlickException e)
			{
				Log.error("\t\t|_ Could not parse sprite (" + e.getMessage() + ")");
				continue;
			}
			spr.setX(x);
			spr.setY(y);
			rtn.add(spr);
			Log.debug("\t\t|_ Sprite[type:: " + type + ", x - " + x + ", y - " + y);
		}
		
		return rtn;
	}
	
}
